原文链接

翻译这篇文章的初衷在于研究一个如何使用动画驱动一个元素的宽或高的变化,起初用过几次官方的 Animated ,但却对某些点一直没有 get 到,导致了这次学习的过程…… 具体详细信息可以看这份 Gist。 ——译者注

流式动画提升了很多应用的用户体验。React Native 注重于性能以此来构建和走出更好的产品。

在本次教程中,我们将学习一些基本的动画。我们将会创建一个 panel 组件。当组件的 body 伸展或是折叠时,我们将会使用一个很棒的动画在下一张图里进行展示。

让我们一起来创建一下我们的项目。如果这是你第一次使用 React Native,请先学习官方网站的入门教程,然后在你的终端里运行如下命令。

$ react-native init Panels

一旦前边的命令执行完毕,我们在 XCode 中打开 ios/Panels.xcodeproj。现在我们可以在 iOS 模拟器上运行应用了。我们看到的应该如下图。

我们已经正确的运行了应用,现在我们开始撸码吧!

创建 panel 组件

我们一起来创建 panel 组件。现在我们只有标题和内容。在工程根目录创建 components 文件夹,我们将会在这里写上我们所有的 JavaScript 类。现在,我们来创建 Panel.js 文件并加入如下代码。

import React,{Component,StyleSheet,Text,View,Image,TouchableHighlight,Animated} from 'react-native'; //Step 1
class Panel extends Component{
constructor(props){
super(props);
this.icons = { //Step 2
'up' : require('./images/Arrowhead-01-128.png'),
'down' : require('./images/Arrowhead-Down-01-128.png')
};
this.state = { //Step 3
title : props.title,
expanded : true
};
}

toggle(){
}

render(){
let icon = this.icons['down']
if(this.state.expanded){
icon = this.icons['up']; //Step 4
} //Step 5
return (
<View style={styles.container} >
<View style={styles.titleContainer}>
<Text style={styles.title}>{this.state.title}</Text>
<TouchableHighlight
style={styles.button}
onPress={this.toggle.bind(this)}
underlayColor="#f1f1f1">
<Image
style={styles.buttonImage}
source={icon}
></Image>
</TouchableHighlight>
</View>
<View style={styles.body}>
{this.props.children}
</View>
</View>
);
}
}
export default Panel;

  1. 我们只需要在 Panel 类中倒入我们正在使用的所有依赖
  2. 然后我们加载了两张图片。我们将会在标题条里添加的可折叠/伸展的按钮上使用它们
  3. 我们给 app 设置了初始的 state 。在这里我们得到了 title 和设置了 expanded 属性为 true
  4. 基于 expanded 属性,我们将会得到正确的图片渲染到 button 上。
  5. 最后,我们渲染了组件。主要是一个带有 title 和 content 的 View。在 body 上我们使用了 this.props.children, 这意味着我们可以在 body 上渲染任意组件。

现在我们需要添加一些基本的样式。

var style = StyleSheet.create({
container : {
backgroundColor: '#fff',
margin:10,
overflow:'hidden'
},
titleContainer : {
flexDirection: 'row'
},
title : {
flex : 1,
padding : 10,
color :'#2a2f43',
fontWeight:'bold'
},
button : {
},
buttonImage : {
width : 30,
height : 25
},
body : {
padding : 10,
paddingTop : 0
}
});

我们只添加了一些简单的颜色,尺寸以及内间距。代码本身就可以解释其本身,因为所有属性和 CSS 时非常类似的。

现在我们已经写好了 Panel 组件,现在让我们在应用中创建一些实例。打开 index.ios.js 文件并加入如下代码。

import React,{AppRegistry,StyleSheet,Text,ScrollView} from 'react-native';
import Panel from './components/Panel'; // Step 1
var Panels = React.createClass({
render: function() {
return ( //Step 2
<ScrollView style={styles.container}>
<Panel title="A Panel with short content text">
<Text>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</Text>
</Panel>
<Panel title="A Panel with long content text">
<Text>Lorem ipsum...</Text>
</Panel>
<Panel title="Another Panel">
<Text>Lorem ipsum dolor sit amet...</Text>
</Panel>
</ScrollView>
);
}
});

var styles = StyleSheet.create({
container: {
flex : 1,
backgroundColor : '#f4f7f9',
paddingTop : 30
},
});
AppRegistry.registerComponent('Panels', () => Panels);

  1. 首先我们导入了我们的新组件。
  2. 然后我们使用一个标题和一些内容来渲染了三个我们的新组件。内容可以是任何东西,但我们将使用 text 来达到示范目的。

牢记重启 packager 来让导入的新图片得以正常工作,否则你将会在加载图片是遇到错误。一旦 packager 重启了,我们看到的应该如下。

运行动画

现在我们的组件已经可以开始添加动画了。当我们的用户点击 伸展/折叠 按钮时,我们需要通过很棒的动画去修改主容器的高度。

第一件需要做的事就是创建一个 Animated.Value 类的实例。这个类的值是用来驱动动画执行过程中的每一帧。在我们的案例中,它将会驱动 height 的值。让我们一起在 Panel 类的构造函数中添加如下代码吧。

this.state = { 
title : props.title,
expanded : true,
animation : new Animated.Value()
};

现在我们在这个组件的 state 中已经有了 animation 属性。我们可以在多个动画中使用同一个实例,例如去驱动透明度,宽度,缩放,甚至任意的样式属性。然而,这个实例一次只能被用于一个动画。

现在我们需要得到我们的 panel 高度的最大值和最小值,我们需要动态去计算它,因为正如前面所提到的,内容里可以有任何东西。为了得到这些尺寸,我们使用 View 组件的 onLayout 事件。这个事件将会在组件被渲染并且所有的大小已经基于样式和内容计算好后被调用。

让我们在组件的渲染方法里添加这些监听器吧。

render(){
//...
return (
<View style={styles.container}>
<View style={styles.titleContainer}
onLayout={this._setMinHeight.bind(this)}> //Step 1
//...
</View>
<View style={styles.body}
onLayout={this._setMaxHeight.bind(this)}> //Step 2
{this.props.children}
</View>
</View>
);
}

  1. 首先我们给标题视图添加 onLayout 监听器。_setMinHeight 方法将会在标题被渲染后调用。
  2. 然后给 body 视图添加 onLayout 监听器。_setMaxHeight 方法将会在 body 被渲染后调用。

现在我们需要在 Panel 类中定义回调方法。

_setMaxHeight(event){ 
this.setState({
maxHeight : event.nativeEvent.layout.height
});
}

_setMinHeight(event){
this.setState({
minHeight : event.nativeEvent.layout.height
});
}

这里所做的仅仅是为了获取高度以及创建新的 state 属性。我们可以在 toggle 方法里使用这些值。主要是用来给动画设置上下限。

现在我们已经有了限制值了,我们可以计算动画的值了。让我们在 toggle 方法里添加如下代码。

toggle(){ //Step 1 
let initialValue = this.state.expanded? this.state.maxHeight + this.state.minHeight : this.state.minHeight,
finalValue = this.state.expanded? this.state.minHeight : this.state.maxHeight + this.state.minHeight;
this.setState({
expanded : !this.state.expanded //Step 2
});

this.state.animation.setValue(initialValue); //Step 3
Animated.spring( //Step 4
this.state.animation,
{
toValue: finalValue
}
).start(); //Step 5
}

  1. 我们设置了起始值和终止值,这里我们需要根据以上步骤来使用这两个限制值。如果组件处于展开状态我们就设置高度为最小值,否则设置为最大值。
  2. 我们需要触发 expanded 值。
  3. 使用 Animated.Value 实例给动画设置初始值。
  4. 我们使用 Animated.spring 方法来运行动画。这个方法负责了所有的计算并通过我们在构造器中声明的 Animated.Value 实例来给动画的每一帧设置值。我们也通过将对象作为第二个参数设置了动画的终止值。
  5. 调用 start 方法来启动所有的计算。

如果我们在模拟器中就这样运行了代码,我们只能看到图片的变化,其他什么都没发生。

困惑的最后一点,就是将 Animated.Value 实例设置到我们想要做动画的组件上。现在万事已经具备,但我们还没有将这些值分配到对应组件的 height 样式上。

让我们按照如下修改 render 方法:

render(){ 
//...
return (
<Animated.View
style={[styles.container,{height: this.state.animation}]}>
//...
</Animated.View>
);
}

第一件事就是我们使用 Animated.View 来替代简单的 View 作为主容器。
现在我们需要选择样式属性来驱动,在这个例子中我们需要驱动的是高度,但我们也可以驱动透明度,或者宽度,甚至其他属性。

高度属性接收了 Animated.Value 实例,这和我们在 Animated.spring 方法中使用的是同一个实例。它将会负责 height 值的所有计算,而且我们在展开或是收缩时都可以看到非常棒的动画。

结论

在处理动画时,我们需要了解的只有三方面。第一,我们需要使用 Animated.Value 来驱动动画执行中的每一帧动画。第二,我们需要使用 Animated.spring 方法来给每一个动画计算值(这里有其他两个方法,我将会在未来的教程中提及)。第三,我们需要使用 Animated.View 组件来替代常规的 View 并将我们所计算出来的值分配给我们需要驱动动画的样式属性上。

精简的动画可以为你的应用提升用户体验。我们可以创建更多复杂的动画,但是理念都是一样的。你可以在 Github 上下载这篇教程的代码 。如果你有问题可以在 Twitter 上给我留言。

也可以使用在本文下面留言和我讨论哦~
译者已经将其封装为库,传送门
——译者注

donation